<?php
// **************************************************************
// | Name: 星空框架 分布式锁
// **************************************************************
// | Version: V5.0
// **************************************************************
// | Author: xkyinzhe <zhonghuaxinxing@sina.cn>
// **************************************************************
// | City: 青岛
// **************************************************************
// | Time： 2021-08-31 10：43
// **************************************************************

namespace app\common\services;

use think\facade\Cache;
use think\facade\Config;

/**
 * redis分布式锁
 * Class Lock
 * @package app\common\service
 */
class Lock
{
    private $retryDelay;
    private $_id;
    private $retryCount;
    private $clockDriftFactor = 0.01;
    private $quorum;
    private $servers = [];
    private $instances = [];
    public static $retData = [];

    public function __construct($retryDelay = 200, $retryCount = 6)
    {
        $this->servers = [[config('cache.stores.redis')['host'], config('cache.stores.redis')['port'], $this->clockDriftFactor]];
        $this->retryDelay = $retryDelay;
        $this->retryCount = $retryCount;
        $this->quorum = min(count($this->servers), (count($this->servers) / 2 + 1));
        $this->instances[] = Cache::handler();
        $this->_id = spl_object_id($this);
        self::$retData[$this->_id] = [];
    }

    /**
     * 加锁
     * @param $keyArr array 锁名
     * @param $ttl    int 过期时间
     * @param int $retryDelay
     * @return array|bool
     */
    public function lock($keyArr = [], $ttl = 10000, $retryDelay = 0)
    {
        if (empty($keyArr)) {
            return true;
        }
        if ($retryDelay) {
            $this->retryDelay = $retryDelay;
        }
        $retry = $this->retryCount;
        do {
            $n = 0;
            $startTime = microtime(true) * 1000; // 当前时间戳的下一秒
          $drift = ($ttl * $this->clockDriftFactor) + 2; //102
          $prefix = config('cache.stores.redis')['prefix'];
            foreach ($this->instances as $instance) {
                foreach ($keyArr as $_key) {
                    $token = uniqid();
                    $validityTime = $ttl - (microtime(true) * 1000 - $startTime) - $drift;
                    if ($validityTime <= 0) {
                        continue;
                    }
                    if (self::set($instance, $prefix, $_key, $token, $ttl)) {
                        array_push(
                            self::$retData[$this->_id],
                            [
                              'validity' => $validityTime,
                              'resource' => $prefix . $_key,
                              'token' => $token,
                          ]
                        );
                        $n++;
                    }
                }
            }
            if ($n >= $this->quorum) {
                return self::$retData[$this->_id];
            }
            $delay = mt_rand(floor($this->retryDelay / 2), $this->retryDelay);
            usleep($delay * 1000);
            $retry--;
        } while ($retry > 0);
        return false;
    }
    /**
     * @xk 建立redis缓存锁
     *
     * @method set
     * @param  [type]     $instance                  [description]
     * @param  [type]     $prefix                    [description]
     * @param  [type]     $_key                      [description]
     * @param  [type]     $token                     [description]
     * @param  [type]     $ttl                       [description]
     * @param  integer    $n                         [description]
     * @Time   2021-08-31
     * @City   青岛
     * @Author xkyinzhe   <zhonghuaxinxing@sina.cn>
     */
    protected function set($instance, $prefix, $_key, $token, $ttl, $n = 0)
    {
        if ($n == 5) {
            return false;
            exit();
        }
        $ret = $instance->set($prefix . $_key, $token, ['NX', 'PX' => $ttl]);
        if (!$ret) {    // 重新加入
            $n++;
            usleep(100 * $n);
            self::set($instance, $prefix, $_key, $token, $ttl, $n);
        } else {
            return true;
        }
    }

    /**
     * 解锁
     * @param $lockData
     */
    public function unlock($lockData = [])
    {
        $script = '
          if redis.call("GET", KEYS[1]) == ARGV[1] then
              return redis.call("DEL", KEYS[1])
          else
              return 0
          end
      ';
        foreach ($this->instances as $instance) {
            foreach ((array)$lockData as $key => $value) {
                $instance->eval($script, [$value['resource'], $value['token']], 1);
            }
        }
        self::$retData[$this->_id] = [];
    }


    public static function _unlock()
    {
        $script = '
          if redis.call("GET", KEYS[1]) == ARGV[1] then
              return redis.call("DEL", KEYS[1])
          else
              return 0
          end
      ';
        $instances = Cache::handler();

        foreach (self::$retData as $key => $v) {
            foreach ($v as $value) {
                $instances->eval($script, [$value['resource'], $value['token']], 1);
            }
        }
        self::$retData = [];
    }

    public function __destruct()
    {
        foreach (self::$retData as $v) {
            $this->unlock($v);
        }
    }
}
